package fit.glike.net;

import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import fit.glike.net.domain.Device;
import fit.glike.net.domain.MacConfigDomain;
import fit.glike.net.domain.NetConfigDomain;
import fit.glike.net.util.MailUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.parser.JSONParser;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
public class NetworkScanner {

    private static String url = "/cgi-bin/luci";
    private static String operatorUrl = "/ubus";
    private static String effectUrl = "/cgi-bin/luci/admin/uci/apply_rollback";
    private static String confirmUrl = "/cgi-bin/luci/admin/uci/confirm";
    private static String newToken = "";

    public static void setUrlStartStr(String baseUrl) {
        url = baseUrl + url;
        operatorUrl = baseUrl + operatorUrl;
        effectUrl = baseUrl + effectUrl;
        confirmUrl = baseUrl + confirmUrl;
    }

    public static void main(String[] args) {

        // 是否停止扫描，默认为否
        boolean isStop = false;
        String jsonFile = "/home/sophia/application/netConf/conf/netConf.json";
        try {
            // 读取文件内容
            String jsonContent = new String(Files.readAllBytes(Paths.get(jsonFile)));
            log.info("获取到netConf.json信息为: {}", jsonContent);

            // 解析JSON
            JSONParser parser = new JSONParser();
            Object json = parser.parse(jsonContent);
            JSONArray jsonArray = JSONUtil.parseArray(json);
            List<MacConfigDomain> macList = jsonArray.toList(MacConfigDomain.class);
            if (macList != null && !macList.isEmpty()) {
                boolean isNeedCheckNetConfig = false; // 判断是否http请求获取配置信息
                log.info("检索当前局域网内的所有设备...");
                while (!isStop) {
                    List<Device> arpMacList = getArpIpMacName();
                    if (!arpMacList.isEmpty()) {
                        log.info("已检测到当前局域网中{}个设备信息", arpMacList.size());
                        for (MacConfigDomain macInfo : macList) {
                            for (Device device : arpMacList) {
                                if (StringUtils.equals(macInfo.getMacNo().toUpperCase(), device.getMacAddress().toUpperCase())) {
                                    log.info("Mac相同: {}; {}", macInfo, device);
                                    macInfo.setLanIp(device.getIpAddress());
                                    // 获取到小魔盒NetSuip的ip
                                    if (StringUtils.equals(macInfo.getName(), "NetSuip")) {
                                        String baseUrl = String.format("http://%s:18", macInfo.getLanIp());
                                        log.info("获取到小魔盒NetSuip的前缀路径为: {}", baseUrl);
                                        setUrlStartStr(baseUrl);
                                    }
                                    break;
                                }
                            }
                            // 判断是否有找到对应的内网ip
                            if (StringUtils.isNotBlank(macInfo.getLanIp())) {
                                isNeedCheckNetConfig = true;
                            }
                        }
                    }
                    if (isNeedCheckNetConfig) {
                        List<NetConfigDomain> changeNetList = new ArrayList<>();
                        log.info("需要获取已配置的数据, 进行对比....");
                        // 和配置列表中的ip地址做对比，如果ip和端口不一致的话，则做更新
                        // 获取登录auth信息
                        String sysauthStr = getSysauthStr();
                        // 获取登录token信息
                        String tokenStr = getTokenStr(sysauthStr);
                        // 获取已配置的列表信息
                        List<NetConfigDomain> configList = getConfigList(sysauthStr, tokenStr);

                        boolean isNeedEffect = false;
                        boolean isAllUpdSuccess = true;
                        if (!configList.isEmpty()) {
                            log.info("已获取配置过的{}条配置信息", configList.size());
                            for (MacConfigDomain macInfo : macList) {
                                log.info(macInfo.toString());
                                for (NetConfigDomain netConfig : configList) {
                                    // 内网ip不为空且配置的名称相同时，进行内网ip、内网端口、外网端口有一个不同的，则需要进行修改
                                    if (StringUtils.isNotBlank(macInfo.getLanIp()) && StringUtils.equals(macInfo.getName(), netConfig.getName())) {
                                        if (!StringUtils.equals(macInfo.getLanIp(), netConfig.getDestIp()) ||
                                                !StringUtils.equals(macInfo.getLanPort(), netConfig.getDestPort()) ||
                                                !StringUtils.equals(macInfo.getWanPort(), netConfig.getSrcPort())) {
                                            isNeedEffect = true;
                                            // 设置要修改的配置信息
                                            netConfig.setNetConfigByMacInfo(macInfo);
                                            // 添加到变更列表中以备邮件发送
                                            changeNetList.add(netConfig);
                                            // 修改配置信息
                                            boolean isUpdateSuccess = updateConf(netConfig);
                                            if (!isUpdateSuccess) {
                                                isAllUpdSuccess = false;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        // 判断是否有修改，有则做生效操作
                        if (isNeedEffect) {
                            log.info("有修改操作，需要进行生效处理...");
                            boolean isApplicationSuccess = applicationNetConf(sysauthStr);
                            boolean isEffectSuccess = effectNetConf(sysauthStr, tokenStr);
                            boolean confirmSuccess = confirmNetConf(sysauthStr);
                            // 所有更新操作成功, 应用配置成功, 生效配置成功；则发送邮件，结束扫描
                            if (isAllUpdSuccess && isApplicationSuccess && isEffectSuccess && confirmSuccess) {
                                log.info("全部请求皆成功..发送邮件通知, 并停止扫描");
                                log.info("新配置已生效...");
                                String allNetConfigContent = "";
                                String contentDetail = "";
                                if (!changeNetList.isEmpty()) {
                                    for (NetConfigDomain netConfig : changeNetList) {
                                        String customString = netConfig.toCustomString();
                                        contentDetail = (StringUtils.isNotBlank(contentDetail) ? (contentDetail + "<br>") : "") + customString;
                                    }
                                }
                                if (!configList.isEmpty()) {
                                    for (NetConfigDomain netConfig : configList) {
                                        String customString = netConfig.toCustomString();
                                        allNetConfigContent = (StringUtils.isNotBlank(allNetConfigContent) ? (allNetConfigContent + "<br>") : "") + customString;
                                    }
                                }
                                // 发送邮件通知
                                String theme = "配置映射新变动通知";
                                String mailContent = String.format("<p>变更配置的信息:<br><span style=\"color:red;font-weight:600\">%s</span></p><p>全部配置信息<br>%s</p>", contentDetail, allNetConfigContent);
                                MailUtil.sendMail(theme, mailContent);
                                isStop = true; // 停止扫描
                            } else {
                                log.info("请求有失败时，则休息30s后重新请求!");
                                Thread.sleep(30000);
                            }
                        } else {
                            log.info("无新配置改动, 不做任何处理...并停止扫描");
                            isStop = true; // 停止扫描
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error("修改配置信息出现异常：{}", e.getMessage());
            log.error(e.getMessage(), e);
        }

    }

    /**
     * 获取局域网下所有的设备ip、mac、name信息
     *
     * @return
     * @throws IOException
     */
    private static List<Device> getArpIpMacName() throws IOException {
        String shellStr = "/usr/sbin/arp-scan -I enp2s0 --localnet";
        List<Device> devices = new ArrayList<>();
        String ipStartStr = "";
        Process process = Runtime.getRuntime().exec(shellStr);
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
                if (line.startsWith("Interface")) {
                    // 本机ip和mac
                    String[] split = line.split(", ");
                    String mac = split[2].replace("MAC: ", "");
                    String ip = split[3].replace("IPv4: ", "");
                    ipStartStr = ip.substring(0, ip.lastIndexOf("."));
                    devices.add(new Device(ip, mac, "本机"));
                } else {
                    if (line.startsWith(ipStartStr)) {
                        // 获取其他设备的ip和mac信息
                        String[] fields = line.trim().split("\\s+");
                        if (fields.length >= 3) {
                            String ip = fields[0];
                            String mac = fields[1];
                            String name = fields[2];
                            devices.add(new Device(ip, mac, name));
                        }
                    }
                }
            }
            if (!devices.isEmpty()) {
                for (Device device : devices) {
                    System.out.println(device);
                }
            }
        }
        return devices;
    }

    /**
     * 获取 sysauth数据
     *
     * @return
     */
    public static String getSysauthStr() {
        String sysauth = "";
        Map<String, String> reqForm = new HashMap<>();
        reqForm.put("luci_username", "root");
        reqForm.put("luci_password", "1001@.Nvlixuexi");

        log.info("请求url:{} \n请求参数: {}", url, reqForm.toString());
        HttpResponse response = HttpUtil.createPost(url)
                .formStr(reqForm)
                .execute();
        List<String> strings = response.headers().get("Set-Cookie");
        if (strings != null && !strings.isEmpty()) {
            String authStr = strings.get(0);
            sysauth = authStr.split("; path=")[0].replace("sysauth=", "");
        }
        return sysauth;
    }

    /**
     * 获取token数据
     *
     * @param sysauth
     * @return
     */
    public static String getTokenStr(String sysauth) {
        String token = "";
        Map<String, String> header = new HashMap<>();
        header.put("Cookie", String.format("sysauth=%s", sysauth));
        HttpResponse resp = HttpUtil.createGet(url)
                .headerMap(header, true)
                .execute();
        String body = resp.body();
        String flag1 = String.format("\"sessionid\":\"%s\",\"token\":\"", sysauth.replace("sysauth=", ""));
        String flag2 = "\",\"apply_timeout\":";
        String str1 = body.split(flag1)[1];
        token = str1.split(flag2)[0];
        log.info("获取到sysauth: {}", sysauth);
        log.info("获取到token: {}", token);
        return token;
    }

    /**
     * 获取已配置的列表数据
     *
     * @param sysauth
     * @return
     */
    public static List<NetConfigDomain> getConfigList(String sysauth, String token) {
        List<NetConfigDomain> netList = new ArrayList<>();
        String reqBody = String.format("[{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"call\",\"params\":[\"%s\",\"uci\",\"get\",{\"config\":\"firewall\"}]}]", sysauth);
        HttpResponse response = HttpUtil.createPost(operatorUrl)
                .body(reqBody, "application/json")
                .execute();
        String body = response.body();
        JSONArray array = JSONUtil.parseArray(body);
        JSONObject json = JSONUtil.parseObj(array.get(0));
        JSONArray arrayResult = json.getJSONArray("result");
        JSONObject valuesJson = JSONUtil.parseObj(arrayResult.get(1));
        JSONObject values = valuesJson.getJSONObject("values");
        for (Map.Entry<String, Object> value : values) {
            // log.info("{}: {}", value.getKey(), value.getValue());
            JSONObject valueJson = JSONUtil.parseObj(value.getValue());
            String type = valueJson.getStr(".type");
            // 获取类型为重定向 redirect 的配置数据
            if (StringUtils.equals(type, "redirect")) {
                // 筛选出我们需要的配置数据
                String name = valueJson.getStr("name");
                String cfgName = valueJson.getStr(".name");
                String[] proto = null;
                JSONArray protoArray = valueJson.getJSONArray("proto");
                if (protoArray != null && !protoArray.isEmpty()) {
                    List<String> list = protoArray.toList(String.class);
                    proto = list.toArray(new String[0]);
                }
                String src_dport = valueJson.getStr("src_dport");
                String dest_ip = valueJson.getStr("dest_ip");
                String dest_port = valueJson.getStr("dest_port");
                netList.add(new NetConfigDomain(sysauth, token, name, cfgName, proto, src_dport, dest_ip, dest_port));
            }
        }
        if (netList != null && !netList.isEmpty()) {
            for (NetConfigDomain netConf : netList) {
                log.info(netConf.toString());
            }
        }
        return netList;
    }

    /**
     * 修改配置信息
     *
     * @param netConf
     */
    public static boolean updateConf(NetConfigDomain netConf) {
        log.info("要修改的配置信息为: {}", netConf);
        String reqBody = String.format("[{\"jsonrpc\":\"2.0\",\"id\":%s,\"method\":\"call\",\"params\":[\"%s\",\"uci\",\"set\",{\"config\":\"firewall\",\"section\":\"%s\",\"values\":{\"name\":\"%s\",\"proto\":[\"tcp\",\"udp\"],\"src_dport\":\"%s\",\"dest_ip\":\"%s\",\"dest_port\":\"%s\"}}]}]",
                (int) (Math.random() * 90 + 10),
                netConf.getSysauth(),
                netConf.getCfgName(),
                netConf.getName(),
                netConf.getSrcPort(),
                netConf.getDestIp(),
                netConf.getDestPort());
        HttpResponse response = HttpUtil.createPost(operatorUrl)
                .body(reqBody, "application/json")
                .execute();
        log.info("修改配置响应: {}", response);
        if (response.getStatus() == HttpStatus.HTTP_OK) {
            log.info("修改配置响应成功!");
            return true;
        } else {
            log.info("修改配置信息响应: {}", response);
        }
        return false;
    }

    /**
     * 应用配置信息
     *
     * @param sysauth
     */
    public static boolean applicationNetConf(String sysauth) {
        String reqBody = String.format("[{\"jsonrpc\":\"2.0\",\"id\":34,\"method\":\"call\",\"params\":[\"%s\",\"session\",\"access\",{\"scope\":\"uci\",\"object\":\"firewall\",\"function\":\"write\"}]}]",
                sysauth);
        HttpResponse response = HttpUtil.createPost(operatorUrl)
                .body(reqBody, "application/json")
                .execute();
        log.info("应用配置信息响应: {}", response);
        if (response.getStatus() == HttpStatus.HTTP_OK) {
            log.info("应用配置信息响应成功!");
            return true;
        } else {
            log.info("应用配置信息响应: {}", response);
        }
        return false;
    }

    /**
     * 确认并生效配置信息
     *
     * @param sysauth
     */
    public static boolean effectNetConf(String sysauth, String token) {
        Map<String, String> reqForm = new HashMap<>();
        reqForm.put("sid", sysauth);
        reqForm.put("token", token);
        Map<String, String> header = new HashMap<>();
        header.put("Cookie", String.format("sysauth=%s", sysauth));
        HttpResponse response = HttpUtil.createPost(effectUrl)
                .formStr(reqForm)
                .headerMap(header, true)
                .execute();
        log.info("确认并生效配置信息响应: {}", response);
        String body = response.body();
        JSONObject bodyJson = JSONUtil.parseObj(body);
        newToken = bodyJson.getStr("token");

        if (response.getStatus() == HttpStatus.HTTP_OK) {
            log.info("确认并生效配置信息响应成功!");
            return true;
        } else {
            log.info("确认并生效配置信息响应: {}", response);
        }
        return false;
    }

    /**
     * 二次确认生效配置信息
     *
     * @param sysauth
     */
    public static boolean confirmNetConf(String sysauth) {
        Map<String, String> reqForm = new HashMap<>();
        reqForm.put("token", newToken);
        Map<String, String> header = new HashMap<>();
        header.put("Cookie", String.format("sysauth=%s", sysauth));
        HttpResponse response = HttpUtil.createPost(confirmUrl)
                .formStr(reqForm)
                .headerMap(header, true)
                .execute();
        log.info("二次确认生效配置信息响应: {}", response);
        if (response.getStatus() == HttpStatus.HTTP_OK) {
            log.info("二次确认生效配置信息响应成功!");
            return true;
        } else {
            log.info("二次确认并生效配置信息响应: {}", response);
        }
        return false;
    }


    public static void main1(String[] args) throws IOException, InterruptedException {
        // 是否进行初始扫描  程序的入口点，表示程序从这里开始执行
        boolean initSearch = true;
        //一个布尔变量，用于标记是否执行初始扫描。如果设置为 true，则程序会执行初始扫描；如果
        //设置为 false，则直接进入主循环。
        if (initSearch) {
            //果执行初始扫描的标志为 true，则执行以下代码块。
            log.info("正在扫描当前网段所有ip，预计耗时1分钟....");
            //打印提示消息，表示程序正在执行初始扫描。
            String netSegment = getNetSegment();
            //调用 getNetSegment() 方法，获取当前局域网的网段信息
            if (netSegment != null) {
                pingNetSegmentAll(netSegment);
            } else {
                //如果获取网段信息失败，则打印错误消息并退出程序。
                log.info("Failed to get network segment.");
                return;
            }
        }
        //声明一个 Device 类型的列表变量 last，用于保存上一次扫描到的设备列表。初始值为 null。
        List<Device> last = null;
        while (true) {
            //进入主循环，程序会持续执行直到手动终止。
            List<Device> currentDevices = getArpIpMac();
            //调用 getArpIpMac() 方法，获取当前局域网中在线的设备列表。
            if (last == null) {
                //如果上一次扫描到的设备列表为空（即第一次扫描），则执行以下代码块。
                log.info("当前在线的设备：");
                //打印提示消息，表示当前在线的设备列表即将输出
                printDevices(currentDevices);
            } else {
                //如果不是第一次扫描，则执行以下代码块。结束新上线设备的判断。
                List<Device> online = getOnlineDevices(last, currentDevices);
                //调用 getOnlineDevices() 方法，获取新上线的设备列表。
                if (!online.isEmpty()) {
                    //如果新上线的设备列表不为空，则执行以下代码块。
                    log.info("新上线设备：");
                    printDevices(online);
                }
                //调用 getOfflineDevices() 方法，获取刚下线的设备列表。
                List<Device> offline = getOfflineDevices(last, currentDevices);
                if (!offline.isEmpty()) {
                    //如果刚下线的设备列表不为空，则执行以下代码块
                    log.info("刚下线设备：");
                    printDevices(offline);
                }
            }
            //结束主循环的大括号
            TimeUnit.SECONDS.sleep(5);
            pingIpList(getIpAddresses(currentDevices));
            last = currentDevices;
        }
    }

    private static String getNetSegment() throws IOException {
        Process process = Runtime.getRuntime().exec("arp -a");
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"))) {
            String line;
            Pattern pattern = Pattern.compile("(\\d+\\.\\d+\\.\\d+)\\.\\d+");
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("接口") || line.startsWith("Interface")) {
                    Matcher matcher = pattern.matcher(line);
                    if (matcher.find()) {
                        return matcher.group(1);
                    }
                }
            }
        }
        return null;
    }

    private static void pingNetSegmentAll(String netSegment) {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        for (int i = 1; i < 255; i++) {
            String ip = netSegment + "." + i;
            executor.submit(() -> {
                try {
                    Runtime.getRuntime().exec("ping -w 1 -n 1 " + ip);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static List<Device> getArpIpMac() throws IOException {
        List<Device> devices = new ArrayList<>();
        Process process = Runtime.getRuntime().exec("arp -a");
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"))) {
            String line;
            boolean headerParsed = false;
            while ((line = reader.readLine()) != null) {
                log.info(line);
                if (line.startsWith("接口") || line.startsWith("Interface")) {
                    continue;
                }
                if (!headerParsed) {
                    headerParsed = true;
                    continue;
                }
                String[] fields = line.trim().split("\\s+");
                if (fields.length >= 3 && "动态".equals(fields[2])) {
                    devices.add(new Device(fields[0], fields[1]));
                }
            }
        }
        return devices;
    }

    private static void pingIpList(List<String> ips) throws InterruptedException {
        log.info("正在扫描在线列表");
        ExecutorService executor = Executors.newFixedThreadPool(4);
        List<Future<?>> futures = new ArrayList<>();
        for (String ip : ips) {
            futures.add(executor.submit(() -> {
                try {
                    Runtime.getRuntime().exec("ping -w 1 -n 1 " + ip);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }));
        }
        for (Future<?> future : futures) {
            try {
                future.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
    }

    private static List<Device> getOnlineDevices(List<Device> last, List<Device> current) {
        List<Device> online = new ArrayList<>(current);
        online.removeAll(last);
        return online;
    }

    private static List<Device> getOfflineDevices(List<Device> last, List<Device> current) {
        List<Device> offline = new ArrayList<>(last);
        offline.removeAll(current);
        return offline;
    }

    private static List<String> getIpAddresses(List<Device> devices) {
        List<String> ips = new ArrayList<>();
        for (Device device : devices) {
            ips.add(device.getIpAddress());
        }
        return ips;
    }

    private static void printDevices(List<Device> devices) {
        for (Device device : devices) {
            log.info(device.toString());
        }
    }


}

